package com.anb.net;

import com.anb.common.CyclicCounter;
import com.anb.common.Log;
import com.anb.common.Task;
import com.anb.common.Timeout;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Map;

public class Connection extends ConnectionBase {
    public Connection(IRunner runner, SocketChannel channel, Dispatcher dispatcher) {
        super(runner, channel, HEAD_LEN + Packet.HEAD_LEN);
        this.dispatcher = dispatcher;
    }

    private Dispatcher dispatcher;
    private CyclicCounter idCounter = new CyclicCounter(1, 65535);

    public static final int DEFAULT_TIMEOUT = 10000;

    public static class RequestOptions {
        public long timeout;
    }

    public static final RequestOptions DEFAULT_REQUEST_OPTIONS;
    static {
        RequestOptions requestOptions = new RequestOptions();
        requestOptions.timeout = DEFAULT_TIMEOUT;
        DEFAULT_REQUEST_OPTIONS = requestOptions;
    }

    public interface ResponseHandler<T> {
        void success(T t);
        void error(Error.Code error, Object obj);
    }

    private class RequestWait {
        public long timeoutId;
        public ResponseHandler responseHandler;
    }

    private Packet packet;
    private CmdHandler cmdHandler;

    @Override
    protected int handleHead(ByteBuffer buffer) {
        Packet packet = new Packet(dispatcher);
        CmdHandler cmdHandler;

        try {
            packet.deserializeHead(buffer);
            cmdHandler = dispatcher.getCmdHandler(packet.cmd);
        } catch (Exception e) {
            Log.Get().error(getAlias(), e);
            return -1;
        }

        this.packet = packet;
        this.cmdHandler = cmdHandler;

        return cmdHandler.sizeLimit;
    }

    @Override
    protected void handleBody(final ByteBuffer buffer) {
        final Packet packet = this.packet;
        final CmdHandler cmdHandler = this.cmdHandler;
        Task.run(new Runnable() {
            @Override
            public void run() {
                _handleBody(packet, cmdHandler, buffer);
            }
        });
    }
    protected void _handleBody(Packet packet, CmdHandler cmdHandler, ByteBuffer buffer) {
        try {
            packet.deserializeBody(buffer);
        } catch (Exception e) {
            Log.Get().error(getAlias() + " packet parse error", e);
            stop();
            return;
        }

        String requestDescription = String.format("%s %s/id:%d", getAlias(), cmdHandler.description, packet.id);

        if (packet.response) {
            RequestWait requestWait;
            synchronized (requests) {
                requestWait = requests.remove(packet.id);
            }
            if (requestWait == null) {
                Log.Get().error(requestDescription + " RequestWait not found");
                return;
            }
            Timeout.instance().remove(requestWait.timeoutId);

            if (packet.error == Error.Code.None) {
                Log.Get().info(requestDescription + " response");
                requestWait.responseHandler.success(packet.data);
            } else {
                Log.Get().error(requestDescription + " response " + packet.error.toString() + ": " + packet.data.toString());
                requestWait.responseHandler.error(packet.error, packet.data);
            }
            return;
        }

        if (cmdHandler == null) {
            return;
        }

        Packet retPacket;
        try {
            Log.Get().info(requestDescription + " enters");
            Object ret = cmdHandler.handle(this, packet.data);
            if (!packet.needResponse) {
                return;
            }

            retPacket = new Packet(dispatcher);
            retPacket.id  = packet.id;
            retPacket.cmd = packet.cmd;
            retPacket.type = cmdHandler.outBody;
            retPacket.response = true;
            retPacket.needResponse = false;
            retPacket.error = Error.Code.None;
            retPacket.data = ret;
        } catch (InvocationTargetException e) {
            Throwable t = e.getTargetException();
            Log.Get().error(requestDescription + " handle error", e);
            String error = String.format("%s handle error", getAlias());
            retPacket = buildErrorPacket(dispatcher, packet, error, t);
        } catch (Exception e) {
            String error = requestDescription + " handle error";
            Log.Get().error(error, e);
            retPacket = buildErrorPacket(dispatcher, packet, error, e);
        }
        Log.Get().info(requestDescription + " exits");
        if (!packet.needResponse) {
            Log.Get().info(requestDescription + " needResponse=false");
            return;
        }
        ByteBuffer[] segments;
        try {
            segments = retPacket.serialize();
        } catch (Exception e) {
            String error = requestDescription + " serialize error";
            Log.Get().error(error, e);
            retPacket = buildErrorPacket(dispatcher, packet, error, e);
            try {
                segments = retPacket.serialize();
            } catch (Exception e1) {
                Log.Get().error(e1);
                return;
            }
        }
        try {
            sendLengthy(segments);
        } catch (IOException e) {
            Log.Get().error(requestDescription + " send error", e);
        }
    }

    private static Packet buildErrorPacket(Dispatcher dispatcher, Packet packet, String text, Throwable e) {
        Packet retPacket = new Packet(dispatcher);
        retPacket.id  = packet.id;
        retPacket.cmd = packet.cmd;
        retPacket.type = Packet.BodyType.String;
        retPacket.response = true;
        retPacket.needResponse = false;
        retPacket.error = Error.Code.RuntimeException;
        retPacket.data = String.format("%s %s\n%s", text, e.getMessage(), e.toString());
        return retPacket;
    }

    private Map<Short, RequestWait> requests = new HashMap<Short, RequestWait>();

    protected  <T> void requestAsync(CmdHandler cmdHandler, Object data,
                                     final ResponseHandler<T> responseHandler, RequestOptions requestOptions) throws Exception {
        final short id = (short)idCounter.next();
        final String requestDescription = String.format("%s %s/id:%d", getAlias(), cmdHandler.description, id);
        Log.Get().info(requestDescription + " request");
        boolean needResponse = responseHandler != null;

        Packet packet = new Packet(dispatcher);
        packet.id = id;
        packet.cmd = cmdHandler.cmd;
        packet.type = cmdHandler.inBody;
        packet.response = false;
        packet.needResponse = needResponse;
        packet.error = Error.Code.None;
        packet.data = data;

        if (needResponse) {
            RequestWait requestWait = new RequestWait();
            requestWait.responseHandler = responseHandler;
            requestWait.timeoutId = Timeout.instance().add(requestOptions.timeout, new Timeout.Callback() {
                @Override
                public void timeout() throws Exception {
                    synchronized (requests) {
                        if (requests.remove(id) == null) {
                            return;
                        }
                    }
                    responseHandler.error(Error.Code.RequestTimeout, requestDescription + " timeout");
                }
            });
            synchronized (requests) {
                requests.put(id, requestWait);
            }
        }

        ByteBuffer[] segments = packet.serialize();
        sendLengthy(segments);
    }

    private class ResponseContainer<T> {
        public T t;
        public Error.Code error = Error.Code.None;
        public Object obj;
    }

    public <T> T request(CmdHandler cmdHandler, Object data, RequestOptions requestOptions) throws Exception {
        final ResponseContainer<T> container = new ResponseContainer<T>();
        requestAsync(cmdHandler, data, new ResponseHandler<T>() {
            @Override
            public void success(T t) {
                container.error = Error.Code.None;
                container.t = t;

                synchronized (container) {
                    container.notify();
                }
            }
            public void error(Error.Code error, Object obj) {
                container.error = error;
                container.obj = obj;

                synchronized (container) {
                    container.notify();
                }
            }
        }, requestOptions);

        synchronized (container) {
            container.wait();
        }

        if (container.error != Error.Code.None) {
            throw new Error(container.error, container.obj);
        }
        return container.t;
    }

    public <T> void requestAsync(String cmd, Object data,
                                     ResponseHandler<T> responseHandler, RequestOptions requestOptions) throws Exception {
        requestAsync(dispatcher.getCmdHandler(cmd), data, responseHandler, requestOptions);
    }

    public <T> void requestAsync(Short cmd, Object data,
                                     ResponseHandler<T> responseHandler,
                                     RequestOptions requestOptions) throws Exception {
        requestAsync(dispatcher.getCmdHandler(cmd), data, responseHandler, requestOptions);
    }

    public <T> void requestAsync(String cmd, Object data,
                                 ResponseHandler<T> responseHandler) throws Exception {
        requestAsync(dispatcher.getCmdHandler(cmd), data, responseHandler, DEFAULT_REQUEST_OPTIONS);
    }

    public <T> void requestAsync(Short cmd, Object data,
                                 ResponseHandler responseHandler) throws Exception {
        requestAsync(dispatcher.getCmdHandler(cmd), data, responseHandler, DEFAULT_REQUEST_OPTIONS);
    }

    public <T> T request(String cmd, Object data, RequestOptions requestOptions) throws Exception {
        return request(dispatcher.getCmdHandler(cmd), data, requestOptions);
    }

    public <T> T request(Short cmd, Object data, RequestOptions requestOptions) throws Exception {
        return request(dispatcher.getCmdHandler(cmd), data, requestOptions);
    }

    public <T> T request(String cmd, Object data) throws Exception {
        return request(cmd, data, DEFAULT_REQUEST_OPTIONS);
    }

    public <T> T request(Short cmd, Object data) throws Exception {
        return request(cmd, data, DEFAULT_REQUEST_OPTIONS);
    }
}
